package com.web.uid.impl;

import com.web.farme.pojo.BizException;
import com.web.uid.BitsAllocator;
import com.web.uid.IdGenerator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.time.DateFormatUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.TimeUnit;


@Slf4j
@Component
public class DefaultIdGenerator implements IdGenerator, InitializingBean {

    @Value("${idGenerator.timeBits:28}")
    protected int timeBits;
    @Value("${idGenerator.workerBits:22}")
    protected int workerBits;
    @Value("${idGenerator.seqBits:13}")
    protected int seqBits;
    @Value("${idGenerator.epochStr:1622875679513}")
    protected String epochStr;

    protected BitsAllocator bitsAllocator;
    protected long workerId;

    /**
     * Volatile fields caused by nextId()
     */
    protected long sequence = 0L;
    protected long lastSecond = -1L;

    //2021-6-5 00：00
    protected long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(1622822400000L);


    public static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

    @Override
    public void afterPropertiesSet() {
        //初始化
        bitsAllocator = new BitsAllocator(timeBits, workerBits, seqBits);

        log.info("Initialized bits(1, {}, {}, {}) for workerID:{}", timeBits, workerBits, seqBits, workerId);
    }


    @Override
    public synchronized long getUID() {
        try {
            return nextId();
        } catch (Exception e) {
            log.error("Generate unique id exception. ", e);
            throw new BizException("创建UID异常\r\n" + e);
        }
    }

    protected synchronized long nextId() {
        long currentSecond = getCurrentSecond();
        // 如果时间往后那么不生成id
        if (currentSecond < lastSecond) {
            long refusedSeconds = lastSecond - currentSecond;
              throw new BizException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
        }
        //在同一秒, 递增sequence
        if (currentSecond == lastSecond) {
            sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
            // 超过最大sequence, 等待下一秒生成
            if (sequence == 0) {
                currentSecond = getNextSecond(lastSecond);
            }
            // 在不同时间，从0开始生成
        } else {
            sequence = 0L;
        }
        lastSecond = currentSecond;
        // 生成uid
        return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
    }


    @Override
    public String parseUID(long uid) {
        long totalBits = BitsAllocator.TOTAL_BITS;
        long signBits = bitsAllocator.getSignBits();
        long timestampBits = bitsAllocator.getTimestampBits();
        long workerIdBits = bitsAllocator.getWorkerIdBits();
        long sequenceBits = bitsAllocator.getSequenceBits();

        // 解析UID
        long sequence = (uid << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
        long workerId = (uid << (timestampBits + signBits)) >>> (totalBits - workerIdBits);
        long deltaSeconds = uid >>> (workerIdBits + sequenceBits);

        Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
        String thatTimeStr = DateFormatUtils.format(thatTime, DATETIME_PATTERN);

        // 格式化
        return String.format("{\"UID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"sequence\":\"%d\"}",
                uid, thatTimeStr, workerId, sequence);
    }


    /**
     * 获取当前的秒
     */
    private long getCurrentSecond() {
        long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
        if (currentSecond - epochSeconds > bitsAllocator.getMaxTimestamp()) {
              throw new BizException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
        }
        return currentSecond;
    }


    /**
     * 获取下一秒时间
     */
    private long getNextSecond(long lastTimestamp) {
        long timestamp = getCurrentSecond();
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentSecond();
        }

        return timestamp;
    }


}
